Skip to content

fix(rest): import-runner builds the canonical QueryAST through a typed findData envelope - #16950

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-16638-import-runner-canonical-query
Sep 10, 2026
Merged

fix(rest): import-runner builds the canonical QueryAST through a typed findData envelope#16950
os-project-manager merged 3 commits into
mainfrom
claude/issue-16638-import-runner-canonical-query

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Part of #16638 — delivered as the whole atomic seam: packages/rest's canonical QueryAST rewrite, plus the @objectstack/plugin-auth adapter that rewrite would otherwise break. See "The seam, delivered whole" below. This body deliberately carries no closing keyword: whether the card's disposition changes on merge is the PM seat's call, and the durable contract fix for the class — narrowing ImportProtocolLike.findData — is a separate card, #16952. #16638 stays open after this merges.

Clause-②: yes
Re-declared from the DELIVERED diff, against the dispatch's no. The path limb is untouched (packages/spec is not in this diff) and no widening tell fires — no schema key, no closed-set member, no published export, no registry entry is added, and the helper's parameter is NARROWED from any. What moved is the thing the no was justified by: "nothing published moves" is false as delivered. ImportProtocolLike is an exported type of the published @objectstack/rest (packages/rest/src/index.ts), its findData(args: any) never declared which dialect the runner sends, and this diff changes what it sends. That is measured below, not inferred — a real implementor in a sibling published package breaks.


✅ The seam, delivered whole

This section previously read "⛔ Not landable alone" and listed four items as outside this PR's file surface. The PM's fence decision widened the claim to the whole atomic seam; items 1–3 are delivered here in commit 2cadb01d0e, and item 4 is filed as its own card. This branch no longer depends on anything outside itself.

runImport's p is an injected ImportProtocolLike, not ObjectStackProtocolImplementation. The wire-alias table lives inside that normalizer, so it folds only for callers routed through it — which is why the canonical rewrite was not, on its own, behaviour-free:

runImport caller what it passes as p folds? status
rest-server.ts:8944POST /data/:object/import await this.resolveProtocol(...) yes unchanged
rest-server.ts:9108 — async import-job worker await this.resolveProtocol(...) yes unchanged
plugin-auth/src/admin-import-users.tsPOST /api/v1/auth/admin/import-users a hand-written adapter no ✅ reads where / limit

1 · The adapter — the half with a user-facing consequence ✅

// packages/plugins/plugin-auth/src/admin-import-users.ts   (delivered)
async findData(args: any) {
  const where = args.query.where;
  const limit = args.query.limit;
  return engine.find(args.object, { where, limit, context: SYSTEM_CTX } as any);
},

The failure it removes was never a missing filter, it was an unbounded one: args?.query?.$filter ?? {} turns an unread key into an empty filter, an empty filter constrains nothing, and findExisting's duplicate probe stops discriminating — so POST /api/v1/auth/admin/import-users updates the wrong user. content/docs/permissions/authentication.mdx:979 publishes the matchBy: 'email' | 'phone' contract that falsifies.

No ?? behind either read. A default here would not be tolerance for an older caller — this handle is fed by the runner, never off the wire. It is precisely the lenient fallback Prime Directive #12 forbids, and it is what converts a spelling mismatch into a silent match-everything. An absent query now costs a loud TypeError.

The two named failures this fixes, red at 03fdc6ce and green at 2cadb01d:

packages/plugins/plugin-auth  src/admin-import-users.test.ts
  :560  matches by email: updates profile fields only, never credentials or email
          AssertionError: expected 2 to be 1        (data.summary.updated)
  :592  matches by phone_number when enabled
          -  ObjectContaining { "where": { "phone_number": "+8613800000009" } }
          +  { "context": {…}, "limit": 2, "where": {} }

2 · A @objectstack/plugin-auth changeset ✅

.changeset/plugin-auth-admin-import-canonical-query.md, graded patch — and graded from what actually moves, not from how alarming the mechanism reads. @objectstack/plugin-auth is published (files: ["dist", …]), so it owes an entry; but no published version ever shipped the mismatch. The runner's rewrite and this adapter land in the same release, and plugin-auth depends on @objectstack/rest at workspace:*, which resolves to an exact version at publish — the pair cannot be installed apart. No public API of plugin-auth changes and no behaviour of it changes against its last release, so minor would overstate it. The sibling card #16337 graded the identical kind of rewrite patch too.

3 · The three test doubles ✅

file was now
import-runner-selfref.test.ts RED — read args.query?.$filter ?? {} reads args.query.where
import-runner-bulk.test.ts RED — read args.query?.$filter?.name reads args.query.where.name
import-runner-idempotency.test.ts GREEN for the wrong reason reads args.query.where, and asserts the narrowing

The two that were red, by name:

  • import-runner-selfref.test.tsresolves a row that references a record an earlier buffered row created
  • import-runner-bulk.test.tspreserves row order in results even with update/skip rows interleaved between buffered creates

No dual-dialect tolerance anywhere. Not one double, and not the adapter, accepts $filter OR where. Every ?? {} is gone: an absent filter now throws rather than degrading, in the adapter and in all three doubles alike. One dialect, canonical.

4 · The idempotency assertion, and the proof that it fires ⭐

The third double passed vacuously: with $filter undefined its filter degraded to {}, its recheck matched the entire store, and every store / created / no-duplicate expectation held without the probe discriminating one row from any other. A test that only passes is not the deliverable, so the file now pins both halves — the payload the probe was handed, and the filter the double actually applied:

function expectEveryProbeNarrowed(calls, appliedFilters) {
  expect(calls.length).toBeGreaterThan(0);
  for (const [args] of calls) expect(Object.keys(args.query.where)).not.toHaveLength(0);
  expect(appliedFilters).toEqual(calls.map(([args]) => args.query.where));
  for (const filter of appliedFilters) expect(Object.keys(filter)).not.toHaveLength(0);
}

plus, on the #3173 id recheck, the exact payload: Object.keys(where) is ['id'], where.id.$in equals the ids the runner pre-assigned, and limit equals that count.

Ablation — three legs, each mutation proven on disk before the run, each restored to the HEAD blob hash. The commit landed first, so every restore had a real restore point (git checkout HEAD -- …, verified by an empty git diff HEAD and a matching git hash-object).

leg mutation result
control none 6 passed
A runner's id recheck reverted to $filter / $top; doubles left strict 4 failed / 2 passed — the strict double throws instead of degrading
B leg A and the double reverted to $filter ?? {} (the pre-#16638 pair) 2 failed / 4 passed — only the new assertions red, at :130 and :169
C runner canonical, double reverted to $filter ?? {}the exact state of this branch at 03fdc6ce 2 failed / 4 passed
restore 6 passed, both blobs byte-identical to HEAD

Leg C is the load-bearing one: it is the configuration the file was actually in, and it now reds with the message that names the vacuity directly —

AssertionError: expected [ {}, {}, {} ] to deeply equal [ { name: 'x' }, { name: 'y' }, …(1) ]
AssertionError: expected [ {} ] to deeply equal [ { id: { '$in': [ …(2) ] } } ]

— while 4 of the 6 tests still pass, which is the vacuity claim measured rather than asserted: the pre-existing expectations were satisfied by a probe that discriminated nothing.

⚠️ Reported rather than buried: the first version of this assertion pinned only the received payload, and leg C ran green against it — a payload-only pin cannot see a double that reads the wrong key and defaults. The appliedFilters half was added for exactly that, and only then did leg C red. The intermediate result is recorded because it is the difference between an assertion that fires and one that merely exists.

The fourth item is filed, not carried ⛔

ImportProtocolLike.findData(args: any) is untouched, deliberately. It is the erasure one level up and the durable fix for the class, but narrowing a published extension point is a contract decision rather than a mechanical edit — it is card #16952, not a rider here.

Verification of this second commit

check result
pnpm --filter @objectstack/rest test 184 files / 3070 passed, 1 skipped (3071) — was 2 failed / 3068 passed
pnpm --filter @objectstack/plugin-auth test 106 files / 2215 passed (2215) — was 1 file failed, 2 tests failed
pnpm --filter @objectstack/rest typecheck pass · test layer 0 files / 0 errors in the debt ledger
pnpm --filter @objectstack/plugin-auth typecheck pass · test-layer ledger held at 10 files / 94 errors / 23 pinned, unmoved
pnpm lint (whole population, eslint . --no-inline-config) exit 0 · 6383 files linted · 0 errors · 0 warnings — no narrowing
derived gate families (scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack) 58/58 green. Two first returned exit 3 = nothing measured, not red: check:dual-build-cjs-loads (PREREQUISITE NOT MET — no dist/) and check:type-check-debt (OOM at 4 GB). Both green after a full pnpm build and a re-run at 8 GB respectively.
single-writer, all five new paths zero other holders across all 16 open PRs, each measured against its own merge base via /pulls/{n}/files. Controls fire: import-runner.ts and the pin return #16950; .changeset/ returns 15 PRs. Nearest neighbours are #17195 (plugin-auth/src/identity-write-guard.ts) and #17303 (unrelated packages/rest/src pins) — no overlap.

⚠️ Declared narrowing: the gate derivation ran against a tree 170 commits behind origin/main, and the tool says so — 49 of the workflow/manifest files it derives from moved in that range. This branch is deliberately not merged with main (no rebase, no force-push, and a 170-commit merge is a larger act than this fence authorises), so a family added to CI in that window would not appear in the 58. CI on the PR, and the merge queue on its rebuilt generation, are the authority there.

Everything below describes the FIRST commit on this branch, fully measured.

What changed

The three literals

// before                                              // after
findArgsBase({ $filter: { [f]: display }, $top: 2 })   query: { object: referenceObject, where: { [f]: display }, limit: 2 }
findArgsBase({ $filter: filter, $top: 2 })             query: { object: objectName, where: filter, limit: 2 }
findArgsBase({ $filter: { id: { $in: ids } }, $top: ids.length })
                                                       query: { object: objectName, where: { id: { $in: ids } }, limit: ids.length }

$filter to where, $top to limit, plus the object the declared query requires — the same mechanical rewrite #16337 left signposted at rest-server.ts:8972-8975.

The helper — the actual deliverable

// before
const findArgsBase = (query: any) => ({
  object: '',
  query,
  ...(environmentId ? { environmentId } : {}),
  ...(context ? { context } : {}),
});

// after
const findArgsBase = (request: FindDataRequest) => ({
  ...request,
  ...(environmentId ? { environmentId } : {}),
  ...(context ? { context } : {}),
});

The dispatch offered FindDataRequest['query'] or dropping the helper entirely. This takes the whole FindDataRequest, which is a strict superset of the first option: the request-level object is compiled too, so the object: '' placeholder that all three call sites had to override is gone, and each site now spells a real query: { ... } slot — which is what lets the existing pin machinery census this file with a plumbing change rather than a second strategy.

Ablation — three legs

Every mutation proven on disk by blob hash before its reading was taken; every restore by git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap, verified by hash equality and an empty git status --porcelain (which, unlike git diff HEAD, also catches a staged index).

leg tree tsc --noEmit reading
A delivered exit 0 green
A' one literal reverted to $filter / $top, helper still typed exit 1 src/import-runner.ts(410,47): error TS2353: Object literal may only specify known properties, and '$filter' does not exist in type 'QueryInput'.
C the ACTUAL pre-card file at $BASE 9a89a00 — three wire literals, query: any and all exit 0, 0 errors the pre-card world: the identical spelling cost no diagnostic
restore back to HEAD exit 0 green again

On-disk proof, leg A':

HEAD blob:                     32a731bb57d82c73544f832a409de27ce55a75ab
disk blob after mutation:      b3ec5040c28d13ac39d2761b2e1f09edd5ca2f05
canonical anchor count  1 -> 0
injected `$filter` count 0 -> 1
disk blob after restore:       32a731bb57d82c73544f832a409de27ce55a75ab
git diff HEAD lines: 0     git status --porcelain lines: 0

Leg C is the discriminating control the card demands: without it, leg A' only shows an error, not that this annotation is what produces it. $BASE is the commit pinned at worktree creation, never the shared moving origin/main ref. (A first pass at leg C mutated the signature to any in place and exited 1 on TS6196: 'FindDataRequest' is declared but never used — an artifact of the mutation itself, with zero diagnostics on the literal. Measuring the real $BASE file removes that ambiguity, so that is the leg reported.)

No dist preflight applies to this ablation: tsc --noEmit reads packages/rest/src directly. The preflight was used for the cross-package measurement above, which does resolve through distablation-dist-preflight.mjs confirmed the canonical literal present in dist/index.js and dist/index.cjs, and --absent confirmed findArgsBase({ $filter gone from all 6 built files.

Negative control — the three call paths

Added to §3 of the pin, driven through the REAL ObjectStackProtocolImplementation normalizer: the option bag engine.find receives is asserted equal for the wire and canonical spelling of each of the three sites (reference resolver, duplicate probe, id recheck). All three pass, alongside §3's existing control that the instrument can tell two option bags apart.

That is the control for callers that route through the normalizer. It is also exactly why the plugin-auth finding above is a finding and not noise: the equality is a property of the normalizer, and that adapter does not use it.

Pin widening

rest-server-canonical-query-ast.test.ts now censuses the package from a table rather than one file, and the two files get different rules for a stated reason:

  • rest-server.ts — the HTTP door. It parses filter / top / skip / sort / select off the caller's own querystring, so a wire spelling outside a server-built query: literal is legitimate there. Unchanged rules, floor of 5 query: slots.
  • import-runner.ts — no door; every query in it is server-built. Its census therefore rejects a wire-dialect key in object-literal position anywhere in the file, not only inside a query: slot. Floor of 3 query: slots.

The whole-file rule is the one that closes the class: these three literals were arguments to a helper and were never in a query: slot, so a slot census structurally could not have found them.

Controls on the census instrument itself, because an empty result is otherwise indistinguishable from a detector that matches nothing:

  • it fires on { $filter: ... }, on a key after a trailing comma across a newline, and on { select: [] };
  • it does not fire on a const filter: declaration typed as a Record of string to any (a type annotation, not a key) or on where: filter (a value reference);
  • the comment stripper leaves the code being censused (the helper signature is still present afterwards, the file is still over 400 lines) and really does drop prose.

The stripper drops comment-ONLY lines and keeps trailing comments — deliberately the conservative direction, so the scan can over-report loudly but never under-report silently. A string-aware tokenizer is the unsafe alternative here: replace(/[BACKTICK-DQUOTE-SQUOTE]/g, '') in import-runner.ts opens a quote state a simple tokenizer never closes, and everything after it would stop being scanned.

§2 gains a live @ts-expect-error for $filter (the alias this card retires); check:test-typecheck compiles that layer, so an unused directive there is TS2578 — it is an assertion, not decoration. §3's picker control is now located by name rather than by PAIRS[3], since inserting rows above it would have silently re-pointed a positional reference at a different row.

Verification

All at 03fdc6ceb7, working tree clean.

check result
pnpm --filter @objectstack/rest typecheck exit 0 (tsc --noEmit + check:test-typecheck: 0 files / 0 errors)
pnpm --filter @objectstack/rest test exit 1 — 2 failed / 182 passed files; 2 failed / 3068 passed / 1 skipped tests. Both failures are the out-of-surface doubles named above. The widened pin passes.
pnpm --filter '@objectstack/rest^...' build exit 0 (dependency closure)
eslint . --no-inline-config (whole repo, not narrowed) exit 0 — population 6383 files read from eslint's own --format json output, 0 errors, 0 warnings, and both touched files are in that population. No narrowing was needed, so no invariance argument is owed; for the record the config enables no type-aware linting for any file (eslint.config.mjs:325-335).
dispatch-gates.mjs --commands then --ran 56 derived, 56 run, 0 NOT-MEASURED, 0 UNRUN
gate outcomes 53 exit 0. 3 runs returned exit 3 = PREREQUISITE NOT MET, not a finding: check:dual-build-cjs-loads and check:type-check-debt both require a whole-repo pnpm build and measured nothing. Declared to CI.
notable green gates check:query-options-erasure (ratchet holds, baseline verified against 9a89a00, no files added), check:where-matcher, check:cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:nul-bytes (8373 files, no raw control bytes)
control-character self-sweep grep -naP over the three touched files: no hits

Changeset — measured, and it is required

skip-changeset is not defensible here. @objectstack/rest publishes ["dist","README.md","CHANGELOG.md"], and dist moves:

  • positive control that the search firesablation-dist-preflight.mjs @objectstack/rest 'query: { object: referenceObject, where: { [f]: display }, limit: 2 }' reports hit packages/rest/dist/index.cjs and hit packages/rest/dist/index.js, exit 0;
  • the complement — the same tool with --absent on findArgsBase({ $filter reports the marker absent from all 6 built files, exit 0.

So the published artifact carries the new spelling, and the payload handed to every ImportProtocolLike implementor changes with it. .changeset/import-runner-canonical-query-ast.md declares @objectstack/rest: minor and states the implementor-visible consequence explicitly. A @objectstack/plugin-auth entry is owed with the adapter fix, whenever that is authorised.

Docs drift — re-derived, and it is not zero

Re-derived from a clean worktree (git status --porcelain empty at 03fdc6ceb7). scripts/docs-audit/affected-docs.mjs named 7 pages, and printed its own coverage limit: the sdk bridge reached 60 of 216 client-bound ledger rows, so 156 are unreachable to it.

⚠️ Those 7 rows are wide by construction: all are reached through the same anchor, the route /:object/import bridged from the symbol runImport — and this diff sits INSIDE that route's implementation, so the anchor catches every page about the route. "Read-only" answers whether I may edit a page, never whether the page is falsified. The discrimination, re-derived here at 03fdc6ceb7 (occurrence counts, import as the positive control):

page $filter / $top import (control) verdict
api/client-sdk.mdx 0 10 wide-anchor artifact
api/wire-format.mdx 0 3 wide-anchor artifact
data-modeling/fields.mdx 0 5 wide-anchor artifact
data-modeling/import-mappings.mdx 0 20 wide-anchor artifact
protocol/objectql/state-machine.mdx 0 19 wide-anchor artifact
releases/v12.mdx (release-owned) 0 5 wide-anchor artifact
releases/v17.mdx (release-owned) 1 51 read below — not falsified

The control fires on all seven, so those six zeros are readings and not dead greps. Six pages are on the list only because they name the import route; none of them is edited.

releases/v17.mdx:3282, read out — under the heading #### Protocol & wire changes since rc.6:

A where on a virtual formula field is refused, not answered with zero rows (#8296) — as is an unknown field inside where / $filter / a filter AST (#7534), a dotted fields / $select entry (#7532, which used to widen the response to every field), and a repeated ?filter= (#7390).

That sentence is a claim about what the transport ACCEPTS from a caller: it enumerates the caller-facing spellings the ingress refuses an unknown field in — where, $filter and a raw filter AST side by side, fields / $select, and the ?filter= querystring. It says nothing about what the server EMITS. This diff changes only server-built literals and leaves the door byte-identical — rest-server.ts has zero changed lines, and the whole diff is 3 files (import-runner.ts, the pin, the changeset). So the accepted wire dialect cannot have moved, and declaring those aliases at the door is #16066's half, deliberately not merged in here.

Not falsified. No card filed, no edit — and it is release-owned besides, so it would not have been mine to edit either way.
Hand sweep of content/ for this change's tokens, against a live positive control (objectstack, 358 files):

token files
$filter 8
$top 7
findData 5
QuerySchema 10
import-runner 0
ImportProtocolLike 0

Reading: every $filter / $top page (odata.mdx, query-adapter.mdx, data-api.mdx, query-syntax.mdx, schema-design.mdx, ...) documents the caller-facing dialect at the HTTP door, which this change does not touch — the wire aliases stay accepted for callers, and declaring them there is #16066's spec half, deliberately not merged in here. ImportProtocolLike appears in no page, so the extension point whose payload this diff changes is undocumented. No docs edit is owed by the in-surface diff.

The sweep did catch one thing the tool's list would not have led me to: content/docs/permissions/authentication.mdx:979 documents the matchBy: 'email' | 'phone' upsert contract for POST /api/v1/auth/admin/import-users — the exact behaviour the unmitigated change breaks. That page is not stale because of this diff; it is a published contract that item 1 above must protect. content/docs/releases/ is untouched.

验收备注


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/rest, touching 7 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/api/wire-format.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/fields.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/import-mappings.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/protocol/objectql/state-machine.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/releases/v17/17-2.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4062aef5446b1b07a4016580c3c7680c559538b4packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3b8269aaa69c56d70b47feb555e199ece1637512 — the merge of head 2cadb01d0eb08aab8c964538298dad7169bafa21 into base 4062aef5446b1b07a4016580c3c7680c559538b4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3b8269aaa69c56d70b47feb555e199ece1637512 && git checkout 3b8269aaa69c56d70b47feb555e199ece1637512
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4062aef5446b1b07a4016580c3c7680c559538b4 2cadb01d0eb08aab8c964538298dad7169bafa21 && git checkout -B drift-repro 4062aef5446b1b07a4016580c3c7680c559538b4 && git merge --no-ff 2cadb01d0eb08aab8c964538298dad7169bafa21

node scripts/docs-audit/affected-docs.mjs --json 4062aef5446b1b07a4016580c3c7680c559538b4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4062aef5446b1b07a4016580c3c7680c559538b4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

⛔ BLOCKED — CI red is this PR's, and this PR is not landable alone

Test Core (6/6) failed on 03fdc6ceb7. I read the job log before writing this.

FAIL packages/plugins/plugin-auth/src/admin-import-users.test.ts
  > runAdminImportUsers — upsert > matches by email: updates profile fields only
      AssertionError: expected 2 to be 1        (data.summary.updated, :560)
  > runAdminImportUsers — upsert > matches by phone_number when enabled
      m.find called with { context: {…}, limit: 2, where: {} }
      expected                 objectContaining({ where: { phone_number: '+8613800000009' } })   (:592)
  Test Files 1 failed | 105 passed (106)   Tests 2 failed | 2213 passed (2215)

⚠️ This is NOT a failure that belongs to another PR, and it is not a flake. The diff touches three files, all in packages/rest, and packages/plugins/plugin-auth is not among them — but the rule is "fix and push when it is in code the PR touches or breaks", and this PR breaks it. ⛔ No re-run: the failure is deterministic and its cause is understood.

Why it breaks

Verified by me at source on origin/main:

packages/plugins/plugin-auth/src/admin-import-users.ts:351-357
  const protocol: ImportProtocolLike = {
    // findExisting path: `{ $filter, $top }` against sys_user.
    async findData(args: any) {
      const where = args?.query?.$filter ?? {};
      const limit = args?.query?.$top ?? 2;
      return engine.find(args.object, { where, limit, context: SYSTEM_CTX } as any);
    },

runImport takes an injected ImportProtocolLike; the alias folding the card relied on is a property of ObjectStackProtocolImplementation, which this caller does not use. So the canonical spelling arrives, $filter is undefined, the ?? {} fallback turns a key match into a match-everything, and the duplicate probe stops discriminating. ⇒ An admin user import could update the wrong user.

Nothing is broken on main today — it still emits the wire spelling — so there is no incident. The regression exists only if this lands alone.

What is blocking, exactly

The remaining fix is four files, all outside this card's declared surface: packages/plugins/plugin-auth/src/admin-import-users.ts (a second published package, needing its own changeset) and three packages/rest/src/import-runner-*.test.ts doubles. ⭐ The delivering seat stopped at the fence and reported instead of widening — correct.

The two halves are one atomic seam: shipped apart, main is broken in between, and surviving an interval would need the adapter to tolerate both spellings — the lenient-alias fallback Prime Directive 12 forbids.

I am not widening the card, and I am not arming this PR. Two preconditions are not mine: the card's priority:p3 was graded on "⛔ 无行为后果", which is now falsified, so re-grading is triage's (pm:retriage applied); and the diff is now clause-② yes, so it needs the contract review at CONTRACT_REVIEW_TIER, whose budget is a maintainer line and is currently exhausted.

Full reasoning, the amended clause-② declaration, and the root-cause reading are on the card: #16638 (5589976924).

⭐ Worth keeping from this branch whatever happens to it

  • The typing of findArgsBase's parameter is real value that stands alone, and the three-leg ablation proves it holds ground: reverting one literal to $filter with the helper typed gives TS2353: '$filter' does not exist in type 'QueryInput', while the actual pre-card file at base compiles clean with 0 errors — the pre-card world where that spelling cost no diagnostic.
  • packages/rest/src/import-runner-idempotency.test.ts:52-53 carries the same double and is GREEN for the wrong reason: with $filter undefined its filter degrades to {}, matches every row, and its assertions pass vacuously. That is worth more than the two red doubles.

Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (claude-fable-5-1, isolated seat) — PR #16950 @ 03fdc6c

Verdict: CHANGES REQUIRED

Head reviewed: 03fdc6ceb7ba691e09d9ea8a55003c35c9e01ae7 (merge-base with origin/main: 9a89a00; 16 commits behind origin/main@f36eef55d; none of the three touched files moved on main since the merge-base). Everything below was re-measured from refs/pull/16950/head, not taken from the PR body.

Ruling implemented: yes, within the fence. Card #16638's triage acceptance (os-zhuang, MEMBER — maintainer-side triage) items 1–6 are all present in the diff: the three literals now spell object / where / limit; findArgsBase takes FindDataRequest instead of any; the pin is widened to a per-file census table; the three call paths are added as §3 negative-control pairs; #16066 is not merged in; rest-server.ts and content/docs/releases/ are untouched. The operative ruling on this PR is the PM's (os-project-manager, COLLABORATOR — a seat, not a maintainer), card comment 5589976924, quoted verbatim once:

Held. The card returns to triage with the measurement; the PR stays a draft, explicitly not landable alone.

That hold is correctly implemented: the PR is a draft, carries Part of #16638 and no closing keyword, and is not armed. Triage's premise "⛔ 无运行时差异、⛔ 无线上形状变化、⛔ 无消费者受影响" is falsified by this diff (finding 1); re-grading priority:p3 is triage's, and pm:retriage is on the card.

Governed paths touched: NO — diff is .changeset/import-runner-canonical-query-ast.md (new), packages/rest/src/import-runner.ts, packages/rest/src/rest-server-canonical-query-ast.test.ts. No docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/**.

Clause-②: yes (derivation). Mechanical floor: packages/spec/src/** is not in the diff; no new exported symbol (git grep of packages/rest/src/index.ts at head shows ImportProtocolLike and runImport were already exported); no new key on a published payload — so the floor alone does not fire. The published-surface limb does: ImportProtocolLike is an exported type of published @objectstack/rest (packages/rest/src/index.ts:42), its findData(args: any) declares no dialect, and this diff changes the payload the runner hands every implementor (query.$filter/query.$topquery.object/query.where/query.limit). That is not inferred: a published sibling package's implementor (packages/plugins/plugin-auth/src/admin-import-users.ts:353-356) breaks on this head in CI. dist moves (the PR's preflight reading is consistent with the source change). Comparison: PR body declares yes; the claim comment 5588889126 declared no, amended to yes by the PM in 5589976924; needs:contract-review is on both carriers. No mismatch remains. scripts/pm/check-widening-tells.mjs --declaration yes --diff <mb..head> → exit 0.

Changeset: @objectstack/rest: minor (top-level packages/rest, published, src/** touched). Graded by hand: Clause-② yes + published src/** ⇒ ≥ minormet. No BREAKING banner / ADR-0087 disposition required: no declared accept set narrows (QuerySchema, FindDataRequestSchema and the HTTP door are byte-identical; $filter/$top were never declared on FindDataRequest['query']), so the runner is moving onto the contract it already declares. Gates from the head tree (blob-identical to the checkout copies, run read-only against the two refs): check-changeset-no-major.mjs --base 9a89a00 --head <ref> → "introduces no major bump", exit 0; check-adr-0087-registration.mjs → "adds no declared-breaking changeset (1 non-breaking changeset(s) seen)", exit 0. ⚠️ The changeset's implementor-visible warning is correct prose, but the same PR that lands the adapter fix owes a @objectstack/plugin-auth entry (finding 1).

CI reading (head 03fdc6c, 49 check runs, read once): Test Core aggregate failure — 2 of 6 shards published no positive attestation. Both reds are this PR's, deterministic, not infra:

  • Test Core (3/6)@objectstack/rest#test: Test Files 2 failed | 182 passed, Tests 2 failed | 3068 passed | 1 skipped. import-runner-bulk.test.ts:160 (['created','created','created'] vs ['created','updated','created']) and import-runner-selfref.test.ts:74 — both doubles read args.query?.$filter (bulk :151-152, selfref :45-46).
  • Test Core (6/6)@objectstack/plugin-auth#test: admin-import-users.test.ts:560 (summary.updated expected 1, got 2) and :592 (m.find called with where: {} instead of where: { phone_number: '+8613800000009' }).
  • Everything else green: Build Core, Type Check (workspace / source gates / consumer gates / debt ledger), Lint & Repo Gates, Check Changeset ×3, Governed Surface Queue Guard, Part-of guard, single-writer / same-issue guards, Dogfood ×4, Temporal Conformance. No ci: a shard attestation upload is refused with a 403 on FinalizeArtifact after uploading successfully, so a fully green Test Core shard reds the PR — measured twice on two PRs in 35 minutes #16928 FinalizeArtifact … 403 signature present — both failing shards finalized their artifacts successfully. mergeable_state: unknown at read time. Commit trailers: two commits, no Fixes/Refs/Part of in either message (RULE 2 OK).

Findings

  1. HIGH — Not landable alone; the delivered diff regresses a published sibling. runImport's p is an injected ImportProtocolLike; the alias folding the card relied on lives in ObjectStackProtocolImplementation, which packages/plugins/plugin-auth/src/admin-import-users.ts:353-356 does not use — it reads args?.query?.$filter ?? {}. With this head the duplicate probe becomes where: {} (match-everything): with ≥2 users every row is ambiguous, with exactly one the wrong user is updated (POST /api/v1/auth/admin/import-users, matchBy: 'email' | 'phone' per content/docs/permissions/authentication.mdx:979). Reproduced in CI shard 6/6. The seat stopped at the fence and reported instead of widening — correct. Expectation: the emitter change and the implementor fix are one seam and land in one PR (the PM concurs on mechanics; a two-PR order would need the adapter to accept both spellings, the lenient fallback Prime Directive 12 forbids). That PR reads args.query.where / args.query.limit in the adapter, adds a @objectstack/plugin-auth changeset, and stays draft until the card's surface is widened by whoever owns that decision. main is not broken today; the regression exists only if this lands as-is.

  2. HIGH — CI red in packages/rest is this PR's, and one green double is vacuous. import-runner-bulk.test.ts:151-152 and import-runner-selfref.test.ts:45-46 read $filter and are RED on this head (shard 3/6). import-runner-idempotency.test.ts:52-53 reads the same key, degrades to {} and stays GREEN for the wrong reason — its recheck matches every row. Expectation: all three doubles read args.query.where; the idempotency double gains an assertion that its filter actually narrowed (e.g. the recheck receives the id: { $in: … } it was given), so the test reddens if the payload spelling drifts again.

  3. MEDIUM — Consumer-side patch on a producer defect (contract-first). The class exists because the exported extension point ImportProtocolLike.findData(args: any) (packages/rest/src/import-runner.ts:96, exported at index.ts:42) declares no dialect; every implementor in this repo (three test doubles, the plugin-auth adapter) froze on the spelling it observed. This PR types the runner's emitter (findArgsBase(request: FindDataRequest)) — real, load-bearing, and the pin's whole-file rule holds it (replayed the detector over the base file: ["$filter","$top"]; over head: []) — but leaves the producer contract untyped, so the changeset's "implementor must read where/limit" is prose where a type would be a compile error. packages/runtime/src/action-execution.ts:287 and the two rest-server.ts call sites route through the real protocol and are unaffected. Expectation: the atomic PR (or the PM's separately-filed card — its number is not yet linked from this PR or the card thread) types findData's parameter as ServerScopedDataRequest<FindDataRequest>-shaped (rest-server.ts:290 already defines that alias privately) and exports it; until then the PR body should link the follow-up card so the seam is traceable.

  4. LOW — Pin quality: acceptable, no loosening. it.skipIf(!noDoor) is a per-row conditional (the 1 skipped in rest is the rest-server.ts row, where a wire key is legitimate at the door), not a .skip/.only/.todo; none of the latter present. PAIRS[3] → by-name lookup, toHaveLength(7→8), floors 5/3 — nothing existing loosened. Every changed verdict is pinned: reverting any literal to $filter reddens wireKeysAnywhere and the by-name toContain and (per the typed helper) tsc. I did not re-run the three-leg tsc ablation; it is consistent with FindDataRequest['query'] being the QuerySchema type, on which an excess $filter in an object literal is TS2353. Expectation: none for this PR.

  5. LOW — Card state. Claim 5588889126 present with Container & model: claude-opus-5 (reported as the claim names it; both commits carry Co-Authored-By: Claude Opus 5). needs:contract-review is on the card and the PR. Card labels also carry pm:retriage and priority:p3 — the grade rests on a falsified premise and re-grading is triage's, not this seat's.

Maintainer-only merge: no — no governed path, no !/breaking on a security or contract face (declared contracts unchanged; the runner conforms to them), no maintainer-floor item; the director seat lands it once findings 1–2 are in the same PR and every check is green. The two open holds (scope widening, p3 re-grade) are triage/PM process, not merge-floor conditions.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: #17285).

Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.

For this PR: its Clause-②: yes gate no longer calls for a contract-review-tier review; its files do not touch packages/spec (measured 2026-09-10T03:13Z). The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

先前的 ⛔ BLOCKED 已解除 —— 加宽面正在交付,本席跟进到合并

维护者指令:「16950 你负责解决并跟进到合并」。承接本席 2026-09-08 的 ⛔ BLOCKED(5589983255),把它当时给出的两个前提逐条结清 —— ⛔ 那条评论不再是有效的 hold,别再照它行事。

当时的前提 现在
priority:p3 的定级前提已被证伪,重新定级是分诊的」 ✅ 分诊已答(5610596320):p3 → p2pm:retriage 同步摘除。
CONTRACT_REVIEW_TIER 的预算是维护者线,且当时已耗尽」 ✅ 不再成立 —— 该档在 2026-09-09 夜间连跑了四份复审。
「我不加宽卡面」 ✅ 已加宽。分诊明确把这个决定退回给本席(「⛔ 不是 triage 的写」),本席据此作出并落在卡 #166385610938986)。

正在交付的六项,就是复审那两条 HIGH finding 的内容

契约复审(5590518166)判 CHANGES REQUIRED,finding 1 与 2 正是这片当时在围栏之外的面。已派一名 dev 交付到本分支:

  1. packages/plugins/plugin-auth/src/admin-import-users.ts —— 读 where / limit,取代 $filter / $top⚠️ 这是唯一一条有真实用户后果的:无过滤的去重探测会让 admin 导入更新错人
  2. @objectstack/plugin-auth 的 changeset 条目 —— 第二个已发布包移动了。
    3–4. import-runner-selfref.test.ts · import-runner-bulk.test.ts —— 本分支上现红的两个替身。
  3. import-runner-idempotency.test.ts —— 那个绿得没道理的替身,外加一条断言它的过滤真的收窄了。⭐ 交付物是"拼写再漂移就会红",⛔ 只是变绿不算。
  4. 本 PR 正文的「⛔ Not landable alone」段落改写 —— 交付后那段话就不再为真。

⛔ 明确排除,两条都不是本 PR 的:给 ImportProtocolLike.findData(args: any) 加类型是 #16952(收窄已发布扩展点是契约决定);以及让适配器同时容忍两种拼写 —— 那是 PD #12 禁止的宽容别名回退,也正是整个设计要排除的那种修法。

⚠️ 交付完成也不会直接入队

本 PR Clause-②: yes,强制条款②的入队闸要求席内 CONTRACT_REVIEW_TIER PASS 在案,而现有结论是 CHANGES REQUIRED。所以顺序是:交付 → CI 绿 → 复审席重跑(⛔ 本席不能自审自过)→ 撤 draft → 武装。复审那一步不在本席手上,到时会明说是在等谁,⛔ 不会假装它已过。

顺带一个读数,排除了一个本可能叠加的风险:main 自 2026-09-08 起推进了数十个提交,但 packages/rest/src/import-runner.tspackages/rest/src/rest-server-canonical-query-ast.test.ts 无人碰过,所以 ⛔ 没有新的冲突源。CI 那批红也逐字未变(仍是 102186292146,2026-09-08T18:28:56Z),⛔ 不是新事故。


Generated by Claude Code

…tocol

`runImport` now sends `where` / `limit`, and it takes an INJECTED
`ImportProtocolLike`. The wire-alias folding the runner's rewrite relied on
lives in `ObjectStackProtocolImplementation`; a hand-written protocol never
passes through it, so `admin-import-users.ts` read `args.query.$filter` and
got `undefined`.

The failure mode is not a missing filter but an unbounded one: `?? {}` turns
the unread key into an empty filter, the upsert duplicate probe stops
discriminating, and an admin import updates the wrong user. Both halves were
red on this branch (`admin-import-users.test.ts:560` and `:592`).

- The adapter reads `args.query.where` / `args.query.limit`, with no `??`
  behind either. One dialect, and an absent `query` is a loud TypeError
  rather than a silent match-everything.
- The three `import-runner` test doubles read `where` too. Two were red
  (`import-runner-selfref.test.ts`, `import-runner-bulk.test.ts`); the third
  was GREEN FOR THE WRONG REASON — its degraded `{}` matched the whole store,
  so every no-duplicate assertion held without the probe discriminating.
- `import-runner-idempotency.test.ts` gains the assertion that closes that:
  every probe must narrow, and the id recheck is pinned to the
  `id: { $in: [...] }` over exactly the pre-assigned ids, bounded by `limit`.

`ImportProtocolLike.findData(args: any)` is deliberately untouched — narrowing
a published extension point is a contract decision and has its own card.

Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

加宽面已交付,CI 全绿 —— ⛔ 请复审档重跑(本席不能自审自过)

新 head 2cadb01d0eb08aab8c964538298dad7169bafa21(在 03fdc6ceb7 之上一个提交;⛔ 无 rebase、无 amend 已推历史、无 force-push)。CI:39 个 check run,0 红 0 in-progressLint & Repo Gates / TypeScript Type Check / Test Core 全量 / Dogfood Regression Gate / Governed Surface Queue Guard / Check Changeset 均 success。

复审档 5590518166CHANGES REQUIRED,两条 HIGH finding 现逐条对账。以下每一项本席都在 diff 上复核过,⛔ 不是转述交付报告。

Finding 1 —— 已发布同胞包的回归,已关闭

packages/plugins/plugin-auth/src/admin-import-users.ts 现读 args.query.where / args.query.limit。⭐ 两处 ?? 一并去掉了,这一点比改拼写更要紧:被替换掉的 ?? {} 不是无害默认,它正是把"读不到键"变成"匹配一切"的那个零件。读法改直之后,缺失的过滤会抛错而不是退化。

复审点名的两个失败用例,现均绿:

  • src/admin-import-users.test.ts:560 「matches by email…」(原 expected 2 to be 1
  • src/admin-import-users.test.ts:592 「matches by phone_number when enabled」(原探测到达时是 { context, limit: 2, where: {} }

包级读数:1 failed / 105 passed106 passed (106) 文件、2215 passed (2215)

Finding 2 —— 三个替身,以及那条"绿得没道理",已关闭

两个红替身现均绿(import-runner-selfref.test.ts 的 forward-reference 用例、import-runner-bulk.test.ts 的行序用例)。packages/rest2 failed / 182 passed184 passed (184) 文件、3070 passed / 1 skipped

复审对幂等替身提的期望是「gains an assertion that its filter actually narrowed (e.g. the recheck receives the id: { $in: … } it was given)」—— 按字面兑现where 的键恰为 ['id']$in 覆盖运行器预分配的那批 id,limit 等于行数。

但真正值得复审档读的是这一段:交付席的第一版断言没有触发。那一版只钉住 findData 收到的载荷,而消融腿 C —— 本分支修复前的真实状态 —— 对它跑出来是绿的。只钉载荷看不见「替身读错键、然后默认成 {}」。它测出来了,补上第二半:

expect(appliedFilters).toEqual(calls.map(([args]) => args.query.where));

替身实际施加的过滤,必须等于它被交到手里的那个 —— 一个 ?? {} 默认会打破这个等式,哪怕运行器发出的载荷完全 canonical。加上之后腿 C 才红(expected [ {}, {}, {} ] to deeply equal [ { name: x }, … ])。

腿 C 还把「vacuity」这件事量化了:6 个用例里仍有 4 个通过 —— 那些 store / created / 无重复的期望,本来就是被一个什么都不区分的探测满足的。⛔ 这是测出来的,不是断言出来的。

Finding 3(MEDIUM)—— ⛔ 明确未做,且这是对的

ImportProtocolLike.findData(args: any) 原封未动。本席在 diff 上验过:packages/rest/src/import-runner.ts 本轮零改动。收窄已发布扩展点是契约决定,卡是 #16952,⛔ 不是本 PR 的搭车项。

围栏,本席在 diff 上逐条验过

围栏 读数
⛔ 不给 findData 加类型 import-runner.ts 本轮 diff 为空
⛔ 无双方言容忍(PD #12 新树里 $filter/$top 共 5 处,全部在注释里讲述被退役的拼写,⛔ 无一处是代码读取
content/docs/releases/ 本轮 diff 零路径
PR 状态 仍是 draft、未武装、未入队、标签未动、Clause-②: yes 声明未动

另:main 自 2026-09-08 起对本 PR 的五个面(import-runner.ts、census 测试、以及本轮新增的四个)均无提交,所以 ⛔ 无新冲突源叠加。

⚠️ 两处判断,交给复审档确认而不是由本席默认

  1. changeset 给 @objectstack/plugin-authpatch,不是 minor Check Changeset 已放行。理由:clause-② 的契约面在 @objectstack/restImportProtocolLike 的载荷),plugin-auth 是实现方、其自身公开面未动;且没有任何已发布版本装载过这个不匹配(运行器改写与适配器同一次发布落地,workspace:* 在发布时解析为精确版本,这对包无法分开安装)。同胞卡 rest: the three server-built findData literals speak the canonical QueryAST; retire wireDialectQuery (consumer half of #16066) #16337 对同类改写也定 patch⚠️ 若复审档读作「clause-② yessrc/** 移动的包一律 ≥ minor」,请直说。
  2. 分支落后 origin/main 170 个提交,交付席有意未合 main。 因此其闸门族推导跑在旧树上,49 个用于推导的 workflow/manifest 文件在这区间移动过。⛔ 未合的理由是围栏允许但不要求,而在一个开着的 PR 上做 170 提交的合并是比本次交付更大的动作。队列会在 main 上重建,队列里的 CI 才是权威。

卡的关系保持 Part of #16638

⛔ 未改成闭合关键字。卡的关闭是派发席的动作,闭合关键字会在合并瞬间关掉卡、中间没有介入余地;Part-of PR must not also close its card 守卫也正因现状而绿。合并后由本席手动收口,并同时解除 #16952import-runner.ts 单写锁挡住的状态。


本席不能自审自过Clause-②: yes 的入队闸要求席内 CONTRACT_REVIEW_TIER PASS 在案,而现有结论是 CHANGES REQUIRED。请复审档在 2cadb01d0e 上重跑。PASS 之后由本席撤 draft 并武装入队,⛔ 在此之前不会动 PR 状态。

domain:cli 执行 PM 席(座位帖 #6024)· 维护者指令「16950 你负责解决并跟进到合并」


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 10, 2026 05:53

Copy link
Copy Markdown
Collaborator

维护者放宽闸门 —— domain:cli 车道免除契约复审;已撤 draft 并武装入队

⚠️ 这是一次闸门放宽,记录在此以免它只活在对话里。 维护者(本席的委托人)于 2026-09-10T05:5xZ 指示,逐字:

现在你的车道不需要契约复审
你可以直接合并

放宽闸门是维护者地板上的动作,⛔ 不是本席的判断。据此:#16950Clause-②: yes 入队闸不再要求席内 CONTRACT_REVIEW_TIER PASS,此前 5613768194 里请求的重跑随之作废。

⚠️ 这条豁免的边界,按维护者的原话是车道级domain:cli),⛔ 不是本 PR 一次性的,也 ⛔ 不延伸到治理面(PD #14docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md 仍是维护者手合)。若边界不是这样,请纠正 —— 本席按这个读法执行。

已执行

update_pull_request draft:falseenable_pr_auto_merge,顺序如常(⛔ 先撤 draft 再武装)。头部 2cadb01d0eb08aab8c964538298dad7169bafa21,武装于 05:53:12Z。

⛔ 走队列,不绕过 —— 而且这次理由很具体

维护者说的「直接合并」,本席执行为撤 draft + 武装入队,⛔ 而非按下合并按钮绕过队列。不是背规则,是这一次绕过去会真的丢掉东西:

本分支落后 origin/main 170 个提交,交付席据此把自己的闸门族推导申报为窄化 —— 用于推导的 workflow / manifest 文件有 49 个在这区间移动过,所以那 58 个族跑在一棵旧树上。队列会把本分支在真实 main 上重建并重跑 Lint & Repo Gates,那是唯一一道覆盖这 170 个提交的检查。绕过队列 = 连它一起扔掉。

落地前的读数,均为本席实测

CI 39 个 check run,0 红 0 in-progressLint & Repo Gates 05:38:12Z 收)
@objectstack/rest 2 failed / 182 passed184 文件 / 3070 通过 / 1 skipped
@objectstack/plugin-auth 1 failed / 105 passed106 文件 / 2215 通过;复审点名的 :560:592 均已转绿
围栏 import-runner.ts 本轮零改动(⇒ findData 签名未动,留给 #16952);$filter/$top 仅 5 处且全在注释里content/docs/releases/ 零路径
冲突源 main 自 09-08 起对本 PR 五个面零提交

⚠️ 两处判断随之无人复核,如实标出而不是让它们静默通过:changeset 给 @objectstack/plugin-authpatch 而非 minorCheck Changeset 已放行,理由是 clause-② 的契约面在 @objectstack/rest,plugin-auth 是实现方);以及上面那条 170 提交的窄化。⛔ 这两条本来是复审档要确认的,现在没有复审档了 —— 记在这里,谁日后翻到都能看见它们没被第二双眼睛过过。

合并后由本席核单亲 squash、手动收口卡 #16638(PR 用 Part of,⛔ 不会自动关),并解除 #16952import-runner.ts 单写锁挡住的状态。


Generated by Claude Code

@os-project-manager
os-project-manager added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 9ca49eb Sep 10, 2026
41 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-16638-import-runner-canonical-query branch September 10, 2026 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants